// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/protocol/pci.h>

#include <fbl/alloc_checker.h>
#include <fbl/unique_ptr.h>

#include <zircon/compiler.h>
#include <zircon/pixelformat.h>
#include <zircon/types.h>

#include <utility>

#include "backends/pci.h"
#include "block.h"
#include "console.h"
#include "device.h"
#include "ethernet.h"
#include "gpu.h"
#include "input.h"
#include "rng.h"
#include "scsi.h"
#include "socket.h"

static zx_status_t virtio_pci_bind(void* ctx, zx_device_t* bus_device) {
  zx_status_t status;
  pci_protocol_t pci;

  // grab the pci device and configuration to pass to the backend
  if (device_get_protocol(bus_device, ZX_PROTOCOL_PCI, &pci)) {
    return ZX_ERR_INVALID_ARGS;
  }

  zx_pcie_device_info_t info;
  status = pci_get_device_info(&pci, &info);
  if (status != ZX_OK) {
    return status;
  }

  zx::bti bti;
  status = pci_get_bti(&pci, 0, bti.reset_and_get_address());
  if (status != ZX_OK) {
    return status;
  }

  // Due to the similarity between Virtio 0.9.5 legacy devices and Virtio 1.0
  // transitional devices we need to check whether modern capabilities exist.
  // If no vendor capabilities are found then we will default to the legacy
  // interface.
  fbl::unique_ptr<virtio::Backend> backend = nullptr;
  uint8_t offset;
  if (pci_get_first_capability(&pci, PCI_CAP_ID_VENDOR, &offset) == ZX_OK) {
    zxlogf(SPEW, "virtio %02x:%02x.%1x using modern PCI backend\n", info.bus_id, info.dev_id,
           info.func_id);
    backend.reset(new virtio::PciModernBackend(pci, info));
  } else {
    zxlogf(SPEW, "virtio %02x:%02x.%1x using legacy PCI backend\n", info.bus_id, info.dev_id,
           info.func_id);
    backend.reset(new virtio::PciLegacyBackend(pci, info));
  }

  status = backend->Bind();
  if (status != ZX_OK) {
    return status;
  }

  // Now that the backend for this device has been initialized we can
  // compose a device based on the PCI device id
  fbl::unique_ptr<virtio::Device> virtio_device = nullptr;
  switch (info.device_id) {
    case VIRTIO_DEV_TYPE_NETWORK:
    case VIRTIO_DEV_TYPE_T_NETWORK:
      virtio_device.reset(
          new virtio::EthernetDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_BLOCK:
    case VIRTIO_DEV_TYPE_T_BLOCK:
      virtio_device.reset(new virtio::BlockDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_CONSOLE:
    case VIRTIO_DEV_TYPE_T_CONSOLE:
      virtio_device.reset(
          new virtio::ConsoleDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_GPU:
      virtio_device.reset(new virtio::GpuDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_ENTROPY:
    case VIRTIO_DEV_TYPE_T_ENTROPY:
      virtio_device.reset(new virtio::RngDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_INPUT:
      virtio_device.reset(new virtio::InputDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_SOCKET:
      virtio_device.reset(new virtio::SocketDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    case VIRTIO_DEV_TYPE_SCSI:
    case VIRTIO_DEV_TYPE_T_SCSI_HOST:
      virtio_device.reset(new virtio::ScsiDevice(bus_device, std::move(bti), std::move(backend)));
      break;
    default:
      return ZX_ERR_NOT_SUPPORTED;
  }

  status = virtio_device->Init();
  if (status != ZX_OK) {
    return status;
  }

  // if we're here, we're successful so drop the unique ptr ref to the object and let it live on
  __UNUSED auto ptr = virtio_device.release();
  return ZX_OK;
}

static const zx_driver_ops_t virtio_driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = virtio_pci_bind;
  return ops;
}();

ZIRCON_DRIVER_BEGIN(virtio, virtio_driver_ops, "zircon", "0.1", 16)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PCI),
    BI_ABORT_IF(NE, BIND_PCI_VID, VIRTIO_PCI_VENDOR_ID),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_BLOCK),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_CONSOLE),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_ENTROPY),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_NETWORK),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_SCSI),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_BLOCK),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_CONSOLE),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_ENTROPY),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_NETWORK),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_T_SCSI_HOST),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_GPU),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_INPUT),
    BI_MATCH_IF(EQ, BIND_PCI_DID, VIRTIO_DEV_TYPE_SOCKET), BI_ABORT(), ZIRCON_DRIVER_END(virtio)
